
$LOAD_PATH << "lib"

require 'date'
require 'stringio'
require 'erb'
require 'fileutils'
require 'rss/maker'

module Corno
  VERSION = '0.0.2'

  class Item
    attr_accessor :item_id, :date, :title, :tags, :author, :contents

    def update(item)
      @item_id = item[:item_id]  if item.key?(:item_id)
      @date = item[:date]  if item.key?(:date)
      @title = item[:title]  if item.key?(:title)
      @tags = item[:tags]  if item.key?(:tags)
      @author = item[:author]  if item.key?(:author)
      @contents = item[:contents]  if item.key?(:contents)

      self
    end
  end

  class Reader
    def self.parse(str, &block)
      items = []

      StringIO.open(str) do |sio|
        items = Corno::Reader.parse_file(sio, &block)
      end

      items
    end

    def self.foreach(filename, &block)
      items = []

      open(filename) do |file|
        items = Corno::Reader.parse_file(file, &block)
      end

      items
    end

    def self.parse_file(io, &block)
      items = []

      item = {}
      entry_daily = []
      entry_header = {}
      is_prev_line_void = false

      io.each do |line|
        case line
        when /^(\d{4}-\d{1,2}-\d{1,2})(.*)/
          date, author = $1, $2

          unless item.empty?
            entry_daily << item.update(entry_header)
            entry_daily = Corno::Reader.number_id(entry_daily)

            entry_daily.each do |entry|
              if block.nil?
                items << Corno::Item.new.update(entry)
              else
                yield(Corno::Item.new.update(entry))
              end
            end

            item = {}
            entry_daily = []
          end

          entry_header = {:date => Date.parse(date), :author => author}

        when /^\t\*\s+(?:(p:)*)([^:]*):(.*)/
          if is_prev_line_void == false
            next  if item.empty?
            item[:contents] << "\n" + line.gsub(/(^\t)|(\n$)|(\r\n$)/, '')
            next
          end

          entry_daily << item.update(entry_header)  unless item.empty?

          item = {:title => $3, :contents => ''}

          if $1 =~ /^p:/
            item = {}
          else
            categories = $2
            puts "warning: maybe a private tag; line: #{$.}, #{entry_header[:date]}"  if categories =~ /^p$/
            item[:tags] = categories.scan(/\[(.+?)\]/).flatten
          end

          is_prev_line_void = false
        when /^\t?__DATA__.*$/
          break
        when /^\t(.*)/
          is_prev_line_void = false

          if item.empty?
            next
          end

          item[:contents] << "\n" + $1
        when /^\s*$/
          is_prev_line_void = true
          next
        end
      end

      unless item.empty?
        entry_daily << item.update(entry_header)
        entry_daily = Corno::Reader.number_id(entry_daily)

        entry_daily.each do |entry|
          if block.nil?
            items << Corno::Item.new.update(entry)
          else
            yield(Corno::Item.new.update(entry))
          end
        end
      end

      items
    end

    # argv:
    #  items => Array -> Hash
    def self.number_id(items)
      return []  if items.empty?

      if items[0][:date].to_s =~ /^(\d{4}-\d{2}-\d{2})/
        date = $1
      end

      items.length.downto(1) do |i|
        items[items.length - i][:item_id] = "#{date}-#{i}"
      end

      items
    end
  end

  class Index
    def initialize
      @indexes = {
        :monthly => {},
        :daily   => {},
        :tags    => {},
        :items   => [],
      }
    end

    def monthly
      @indexes[:monthly]
    end

    def daily
      @indexes[:daily]
    end

    def tags
      @indexes[:tags]
    end

    def items
      @indexes[:items]
    end

    def <<(other)
      yy, mm, dd = /([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})/.match(other.item_id).to_a.values_at(1, 2, 3)

      other.tags.each do |cat|
        @indexes[:tags][cat] = []  unless @indexes[:tags].key?(cat)
        @indexes[:tags][cat] << other
      end

      @indexes[:monthly]["#{yy}-#{mm}"] = []    unless @indexes[:monthly].key?("#{yy}-#{mm}")
      @indexes[:monthly]["#{yy}-#{mm}"] << other

      @indexes[:daily]["#{yy}-#{mm}-#{dd}"] = []    unless @indexes[:daily].key?("#{yy}-#{mm}-#{dd}")
      @indexes[:daily]["#{yy}-#{mm}-#{dd}"] << other

      @indexes[:items] << other
    end
  end

  class Plugin
    def initialize(config)
      @config = config
    end

    def self.load(config, dir)
      plugin = Corno::Plugin.new(config)

      Dir::glob("#{dir}/**/*.rb") do |file|
        begin
          plugin.instance_eval(File::open(file){|f| f.read }.untaint, file, 1)
#         $stderr.puts "load plugin: #{file}"
        rescue Exception => e
        end
	  end

      plugin
    end
  end

  class Config
    DEFAULT_CONFIG = {
      :clog_file       => 'ChangeLog',

      :blog_title      => 'blog title',
      :blog_url        => 'http://localhost/diary/', 
      :description     => 'blog description',
      :charset         => 'utf-8',

      :style           => 'chalow',
      :plugin_path     => './plugin',

      :index_entries   => 5,
      :rss_entries     => 5,

      :theme_path      => './theme/default/',
      :theme_index     => 'index.rhtml',
      :theme_monthly   => 'monthly.rhtml',
      :theme_page      => 'page.rhtml',
      :theme_css       => 'default.css',

      :archive_path    => './archive/',
      :archive_index   => 'index.html',
      :archive_monthly => '%Y-%m.html',
      :archive_daily   => '%Y-%m-%d.html',
      :archive_tag     => 'tag_%cat/index.html',
      :archive_page    => '%Y-%m-%d-%q.html',
      :rss_filename    => 'blog.rss',
    }

    def initialize
      DEFAULT_CONFIG.each_pair do |key, val|
        Corno.add_attribute(self, key, val)
      end
    end

    def self.load(filename)
      conf = Corno::Config.new

      conf.instance_eval(File::open(filename){|f| f.read }.untaint, "conffile", 1)
      conf.instance_variables.each do |name|
        name = name.sub(/^@/, '')

        unless conf.respond_to?(name)
          conf.instance_eval(<<-METHOD_DEFINE
              def #{name}
                @#{name}
              end

              def #{name}=(v)
                @#{name} = v
              end
            METHOD_DEFINE
          )
        end
      end

      conf
    end
  end

  class Archive
    def initialize(config, plugin, indexes)
      @indexes = indexes
      @config = config
      @plugin = plugin
    end

    def build_rss
      items = @indexes.items
 
      rss = RSS::Maker.make('1.0') do |maker|
        maker.channel.about = "#{@config.blog_url}#{@config.rss_filename}"
        maker.channel.title = @config.blog_title || 'corno'
        maker.channel.description = @config.description || ''
        maker.channel.link = @config.blog_url || 'http://example.com/'

        items[0..@config.rss_entries-1].each do |log_item|
          item = maker.items.new_item

          item.title = log_item.title || ''
          item.link = "#{@config.blog_url}#{log_item.permalink}"
          item.description = log_item.contents || ''
          item.date = Time.parse(log_item.date.to_s) || Time.now
        end
      end

      File::open("#{@config.archive_path}#{@config.rss_filename}", "w") {|w| w.puts rss }
    end

    def build_index_page
      limit = @config.respond_to?(:index_entries) ? @config.index_entries : 5
      erb_binding = Corno::Archive.erb_binding_index_page(@config, @indexes.items[0..limit-1])

      erb = ERB.new(File::open("#{@config.theme_path}/#{@config.theme_index}", 'r').read)
      File::open("#{@config.archive_path}/#{@config.archive_index}", 'w').write(erb.result(erb_binding))
    end

    def build_item_page

      @indexes.items.each do |item|
        filename = Corno::Archive.format_archive_file(item, @config.archive_page)
        erb_binding = Corno::Archive.erb_binding_item_page(@config, item)

        erb = ERB.new(File::open("#{@config.theme_path}/#{@config.theme_page}", 'r').read)
        File::open("#{@config.archive_path}/#{filename}", 'w').write(erb.result(erb_binding))
      end
    end

    def method_missing(name, *argv)
      @plugin.send(name, *argv)
    end

    def self.erb_binding_index_page(config, items)
      binding
    end

    def self.erb_binding_item_page(config, item)
      binding
    end

    def self.format_archive_file(item, format)
      seq_no = /-([0-9]+)$/.match(item.item_id).to_a.values_at(1)

      filename = format
      filename = filename.gsub(/%Y/, item.date.strftime("%Y"))
      filename = filename.gsub(/%m/, item.date.strftime("%m"))
      filename = filename.gsub(/%d/, item.date.strftime("%d"))
      filename.gsub(/%q/, seq_no.to_s)
    end
  end

  def self.run(config)
    encoding_tbl = {
      'sjis'      => 'tosjis',
      'shift-jis' => 'tosjis',
      'euc'       => 'toeuc',
      'euc-jp'    => 'toeuc',
      'utf-8'     => 'toutf8',
    }

    plugin = Corno::Plugin.load(config, config.plugin_path)

    encoding_method = encoding_tbl.key?(config.charset) ? encoding_tbl[config.charset] : 'to_s'
    config.blog_title = config.blog_title.send(encoding_method)
    config.description = config.description.send(encoding_method)

    prev_entry = nil

    indexes = Corno::Index.new

    Corno::Reader.foreach( config.clog_file) do |item|
      prev_entry.next = item  unless prev_entry.nil?

      Corno.add_attribute(item, 'next', nil)
      Corno.add_attribute(item, 'permalink', Corno::Archive.format_archive_file(item, config.archive_page))
      Corno.add_attribute(item, 'prev', prev_entry)

      prev_entry = item

      puts ">> #{item.item_id} #{item.title.tosjis}"

      item.title = item.title.send(encoding_method)
      item.contents = item.contents.send(encoding_method)
      item.tags.each do |cat|
          cat.send(encoding_method)
      end

      # TODO: to style
      item.contents = CornoStyle::to_html(item.contents, plugin)

      indexes << item
    end

    archive = Corno::Archive.new(config, plugin, indexes)

    archive.build_rss
    archive.build_index_page
    archive.build_item_page

    FileUtils::copy("#{config.theme_path}/#{config.theme_css}", "#{config.archive_path}")

    true
  end

  def self.add_attribute(obj, name, value)
    obj.instance_eval %Q{
      @#{name} = value

      def #{name}
        @#{name}
      end

      def #{name}=(v)
        @#{name} = v
      end
    }
  end
end

